package com.google.code.virtualhockey.vhx.sds.fields

import com.google.code.virtualhockey.vhx.sds._
import com.google.appengine.api.datastore.Entity
import java.lang.reflect.Field

/**
 * Base class of all persistable fields.
 */
protected[sds] abstract class SField( val parent: STMetaData[_],
                                      val name: String,
                                      val indexed: Boolean = false,
                                      val dbName: Option[String] = None ) {

  /** Type of the described field.*/
  protected type TRawType

  /** Corresponding GAE type. */
  protected type TGaeType

  /** Class object for the raw field type. */
  protected val clsRaw: Class[TRawType]

  /** Class object for the GAE field type. */
  protected val clsGae: Class[TGaeType]

  /** Indicates whether a value is optional or not (i.e. uses Option[ActualType]). */
  protected var optional: Boolean = false

  /** Underlying Java Field (reflection API) */
  protected val field: Field =
    try {
      val f = parent.cls.getDeclaredField( name )
      val t = f.getType
      if ( t == classOf[Option[_]] ) {
        val aTypeParams = t.getTypeParameters()
        if ( aTypeParams.length == 1 && aTypeParams( 0 ) == clsRaw )
          optional = true
        else
          Predef.error( "Field '" + name + "' is optional, but the type parameter is '" + aTypeParams( 0 ).getName + "'. Expected is '" + clsRaw.getName + "'." )
      } else if ( t != clsRaw )
        Predef.error( "Field '" + name + "' has type '" + f.getType().getName + "' but expected is '" + clsRaw.getName + "'." )
      f.setAccessible( true )
      f
    } catch {
      case ex: NoSuchFieldException => Predef.error( "No field '" + name + "' exists in class '" + parent.cls.getName + "'." )
    }

  /**
   * Checks if the given value is valid for this field and can be persisted. By default,
   * this method provides no implementation. Please override if a special validation is required.
   *
   * @param value the value to be checked
   * @throws RuntimeException if the checked value is not persistable
   */
  protected def validate( value: Option[TRawType] ): Unit = {}

  /**
   * Sets the value of this field in the source STEntity to the given GAE Entity.
   *
   * @param target the GAE Entity to which to set the field's value as property
   * @param src the STEntity from which to read the source value
   */
  protected[sds] def setToGae( target: Entity, src: STEntity[_] ) {
    // get the actual field value, if Option[] is used, then extract the actual value out of
    // the Option. None is considered as being null on the GAE side.
    //
    val valueSds: TRawType = if ( this.optional ) {
      val optValueSds: Option[TRawType] = this.field.get( src ).asInstanceOf[Option[TRawType]]
      if ( optValueSds == None )
        null.asInstanceOf[TRawType]
      else
        optValueSds.get
    } else
      this.field.get( src ).asInstanceOf[TRawType]

    if ( valueSds == null )
      target.setProperty( if ( dbName == None ) name else dbName.get, null )
    else {
      validate( Some( valueSds ) )
      target.setProperty( if ( dbName == None ) name else dbName.get, convertSdsToGae( valueSds ) )
    }
  }

  /**
   * Initialises this field from the GAE.
   *
   * @param target the target instance for which to set the field's value
   * @param src the GAE entity providing the value for this field
   */
  protected[sds] def setFromGae( target: STEntity[_], src: Entity ) {
    val valueGae: TGaeType = src.getProperty( if ( dbName == None ) name else dbName.get ).asInstanceOf[TGaeType]

    // handle null value
    //
    if ( valueGae == null && this.optional )
      this.field.set( target, None )
    else
      Predef.error( "Received null for field'" + this.parent.cls.getName + "#" + this.field.getName + "' but type is not Option[_]!" )

    // non-null values
    //
    val valueSds = convertGaeToSds( valueGae )
    this.field.set( target, if ( this.optional ) Some( valueSds ) else valueSds )
  }

  /**
   * Converts the value from the GAE type into the SDS type. By default, if both types
   * are equal, the passed in value is simply returned. Otherwise, an exception is thrown.
   * Fields are expected to override this method if GAE and SDS types are different.
   *
   * @param value the value to be transformed from the GAE to the SDS type
   */
  protected[sds] def convertGaeToSds( value: TGaeType ): TRawType = {
    if ( this.clsGae == this.clsRaw )
      value.asInstanceOf[TRawType]
    else
      Predef.error( "Method convertGaeToSds not overridden by " + getClass().getName + " although GAE and SDS types are different." )
  }

  /**
   * Converts the value from the SDS type into the GAE type. By default, if both types
   * are equal, the passed in value is simply returned. Otherwise, an exception is thrown.
   * Fields are expected to override this method if GAE and SDS types are different.
   *
   * @param value the value to be transformed from the SDS to the GAE type
   */
  protected[sds] def convertSdsToGae( value: TRawType ): TGaeType = {
    if ( this.clsGae == this.clsRaw )
      value.asInstanceOf[TGaeType]
    else
      Predef.error( "Method convertSdsToGae not overridden by " + getClass().getName + " although GAE and SDS types are different." )
  }
}